"""
Extension Auto-Discovery System

Automatically discovers and loads extensions from plugin directories,
supporting both YAML/JSON config files and Python-based extensions.
"""

import os
import json
import logging
import importlib.util
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Any, Union
from dataclasses import dataclass
from enum import Enum

import yaml

from .base_extension import BaseExtension
from .config_driven_extension import ConfigDrivenExtension
from .config_parser import ExtensionConfigParser

logger = logging.getLogger(__name__)


class DiscoveryResult(Enum):
    """Results of extension discovery."""
    SUCCESS = "success"
    FAILED_VALIDATION = "failed_validation"
    FAILED_LOADING = "failed_loading"
    DUPLICATE_NAME = "duplicate_name"
    MISSING_FILES = "missing_files"


@dataclass
class DiscoveredExtension:
    """Information about a discovered extension."""
    name: str
    version: str
    path: Path
    extension: Optional[BaseExtension]
    result: DiscoveryResult
    error_message: Optional[str] = None
    config_type: Optional[str] = None  # 'yaml', 'json', 'python'
    priority: int = 0


@dataclass
class DiscoveryStats:
    """Statistics from extension discovery process."""
    total_directories: int = 0
    successful_discoveries: int = 0
    failed_discoveries: int = 0
    config_driven_extensions: int = 0
    python_extensions: int = 0
    duplicate_names: int = 0
    errors: List[str] = None
    
    def __post_init__(self):
        if self.errors is None:
            self.errors = []


class ExtensionAutoDiscovery:
    """
    Automatically discover and load extensions from plugin directories.
    
    Supports both YAML/JSON configuration-driven extensions and 
    traditional Python-based extensions.
    """
    
    def __init__(self):
        """Initialize the auto-discovery system."""
        self.config_parser = ExtensionConfigParser()
        self._discovered_extensions: Dict[str, DiscoveredExtension] = {}
        
    def discover_extensions(self, 
                          plugin_dirs: Union[str, Path, List[Union[str, Path]]], 
                          recursive: bool = True,
                          include_patterns: Optional[List[str]] = None,
                          exclude_patterns: Optional[List[str]] = None) -> Tuple[List[BaseExtension], DiscoveryStats]:
        """
        Discover extensions from plugin directories.
        
        Args:
            plugin_dirs: Directory or list of directories to search
            recursive: Whether to search recursively in subdirectories
            include_patterns: Glob patterns to include (e.g., ['*gcr*', '*petsc*'])
            exclude_patterns: Glob patterns to exclude (e.g., ['*test*', '*backup*'])
            
        Returns:
            Tuple of (extensions list, discovery statistics)
        """
        if isinstance(plugin_dirs, (str, Path)):
            plugin_dirs = [plugin_dirs]
            
        plugin_dirs = [Path(p).resolve() for p in plugin_dirs]
        
        stats = DiscoveryStats()
        discovered_extensions = []
        self._discovered_extensions.clear()
        
        for plugin_dir in plugin_dirs:
            if not plugin_dir.exists():
                logger.warning(f"Plugin directory does not exist: {plugin_dir}")
                stats.errors.append(f"Directory not found: {plugin_dir}")
                continue
                
            logger.info(f"Discovering extensions in: {plugin_dir}")
            dir_extensions, dir_stats = self._discover_in_directory(
                plugin_dir, recursive, include_patterns, exclude_patterns
            )
            
            discovered_extensions.extend(dir_extensions)
            stats.total_directories += dir_stats.total_directories
            stats.successful_discoveries += dir_stats.successful_discoveries
            stats.failed_discoveries += dir_stats.failed_discoveries
            stats.config_driven_extensions += dir_stats.config_driven_extensions
            stats.python_extensions += dir_stats.python_extensions
            stats.duplicate_names += dir_stats.duplicate_names
            stats.errors.extend(dir_stats.errors)
        
        # Resolve duplicates and conflicts
        final_extensions = self._resolve_extension_conflicts(discovered_extensions)
        stats.successful_discoveries = len(final_extensions)
        
        logger.info(f"Discovery complete: {len(final_extensions)} extensions loaded")
        return final_extensions, stats
        
    def _discover_in_directory(self, 
                             plugin_dir: Path, 
                             recursive: bool,
                             include_patterns: Optional[List[str]],
                             exclude_patterns: Optional[List[str]]) -> Tuple[List[BaseExtension], DiscoveryStats]:
        """Discover extensions in a single directory."""
        stats = DiscoveryStats(total_directories=1)
        extensions = []
        
        # Get candidate directories
        if recursive:
            candidates = [d for d in plugin_dir.rglob('*') if d.is_dir()]
        else:
            candidates = [d for d in plugin_dir.iterdir() if d.is_dir()]
            
        candidates.append(plugin_dir)  # Include the root directory itself
        
        # Apply include/exclude patterns
        candidates = self._filter_directories(candidates, include_patterns, exclude_patterns)
        
        for candidate_dir in candidates:
            discovered = self._discover_extension_in_path(candidate_dir)
            if discovered and discovered.extension:
                if discovered.name in self._discovered_extensions:
                    # Handle duplicate
                    existing = self._discovered_extensions[discovered.name]
                    logger.warning(f"Duplicate extension name '{discovered.name}': "
                                 f"{existing.path} vs {discovered.path}")
                    stats.duplicate_names += 1
                    
                    # Keep the one with higher priority or more recent
                    if discovered.priority > existing.priority:
                        self._discovered_extensions[discovered.name] = discovered
                        extensions.append(discovered.extension)
                        stats.successful_discoveries += 1
                        self._update_config_stats(stats, discovered)
                else:
                    self._discovered_extensions[discovered.name] = discovered
                    extensions.append(discovered.extension)
                    stats.successful_discoveries += 1
                    self._update_config_stats(stats, discovered)
            elif discovered and discovered.result != DiscoveryResult.SUCCESS:
                stats.failed_discoveries += 1
                if discovered.error_message:
                    stats.errors.append(f"{candidate_dir}: {discovered.error_message}")
                    
        return extensions, stats
        
    def _discover_extension_in_path(self, extension_dir: Path) -> Optional[DiscoveredExtension]:
        """Discover a single extension in the given path."""
        logger.debug(f"Checking for extension in: {extension_dir}")
        
        # Try to find configuration files
        config_candidates = [
            ('extension.yaml', 'yaml'),
            ('extension.yml', 'yaml'),
            ('extension.json', 'json'),
            ('plugin.yaml', 'yaml'),
            ('plugin.yml', 'yaml'),
            ('plugin.json', 'json'),
        ]
        
        for config_file, config_type in config_candidates:
            config_path = extension_dir / config_file
            if config_path.exists():
                return self._load_config_driven_extension(config_path, config_type)
                
        # Try to find Python extension
        python_candidates = [
            'extension.py',
            'plugin.py',
            '__init__.py'
        ]
        
        for python_file in python_candidates:
            python_path = extension_dir / python_file
            if python_path.exists():
                return self._load_python_extension(extension_dir, python_path)
                
        # No valid extension found
        logger.debug(f"No extension configuration found in: {extension_dir}")
        return None
        
    def _load_config_driven_extension(self, config_path: Path, config_type: str) -> DiscoveredExtension:
        """Load a configuration-driven extension."""
        try:
            logger.debug(f"Loading config-driven extension from: {config_path}")
            
            # Parse the configuration
            if config_type == 'yaml':
                with open(config_path, 'r') as f:
                    raw_config = yaml.safe_load(f)
            else:  # json
                with open(config_path, 'r') as f:
                    raw_config = json.load(f)
                    
            # Validate and parse
            config = self.config_parser.parse_config(raw_config)
            
            # Create extension instance
            extension = ConfigDrivenExtension(config, config_path.parent)
            
            return DiscoveredExtension(
                name=extension.name,
                version=extension.version,
                path=config_path.parent,
                extension=extension,
                result=DiscoveryResult.SUCCESS,
                config_type=config_type,
                priority=raw_config.get('metadata', {}).get('priority', 0)
            )
            
        except Exception as e:
            logger.error(f"Failed to load config-driven extension from {config_path}: {e}")
            return DiscoveredExtension(
                name=config_path.parent.name,
                version="unknown",
                path=config_path.parent,
                extension=None,
                result=DiscoveryResult.FAILED_LOADING,
                error_message=str(e),
                config_type=config_type
            )
            
    def _load_python_extension(self, extension_dir: Path, python_path: Path) -> DiscoveredExtension:
        """Load a Python-based extension."""
        try:
            logger.debug(f"Loading Python extension from: {python_path}")
            
            # Import the Python module
            spec = importlib.util.spec_from_file_location(f"extension_{extension_dir.name}", python_path)
            if spec is None or spec.loader is None:
                raise ImportError(f"Could not load module spec from {python_path}")
                
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            
            # Find BaseExtension subclass in the module
            extension_class = None
            for attr_name in dir(module):
                attr = getattr(module, attr_name)
                if (isinstance(attr, type) and 
                    issubclass(attr, BaseExtension) and 
                    attr != BaseExtension):
                    extension_class = attr
                    break
                    
            if extension_class is None:
                raise ImportError(f"No BaseExtension subclass found in {python_path}")
                
            # Create extension instance
            extension = extension_class()
            
            return DiscoveredExtension(
                name=extension.name,
                version=extension.version,
                path=extension_dir,
                extension=extension,
                result=DiscoveryResult.SUCCESS,
                config_type='python',
                priority=getattr(extension, 'priority', 0)
            )
            
        except Exception as e:
            logger.error(f"Failed to load Python extension from {python_path}: {e}")
            return DiscoveredExtension(
                name=extension_dir.name,
                version="unknown",
                path=extension_dir,
                extension=None,
                result=DiscoveryResult.FAILED_LOADING,
                error_message=str(e),
                config_type='python'
            )
            
    def _filter_directories(self, 
                          directories: List[Path], 
                          include_patterns: Optional[List[str]], 
                          exclude_patterns: Optional[List[str]]) -> List[Path]:
        """Filter directories based on include/exclude patterns."""
        if not include_patterns and not exclude_patterns:
            return directories
            
        import fnmatch
        filtered = []
        
        for directory in directories:
            dir_name = directory.name
            
            # Apply include patterns
            if include_patterns:
                if not any(fnmatch.fnmatch(dir_name, pattern) for pattern in include_patterns):
                    continue
                    
            # Apply exclude patterns
            if exclude_patterns:
                if any(fnmatch.fnmatch(dir_name, pattern) for pattern in exclude_patterns):
                    continue
                    
            filtered.append(directory)
            
        return filtered
        
    def _resolve_extension_conflicts(self, extensions: List[BaseExtension]) -> List[BaseExtension]:
        """Resolve conflicts when multiple extensions have the same name."""
        name_to_extensions = {}
        
        for extension in extensions:
            if extension.name not in name_to_extensions:
                name_to_extensions[extension.name] = []
            name_to_extensions[extension.name].append(extension)
            
        resolved_extensions = []
        
        for name, ext_list in name_to_extensions.items():
            if len(ext_list) == 1:
                resolved_extensions.append(ext_list[0])
            else:
                # Multiple extensions with same name - choose based on priority
                logger.warning(f"Multiple extensions found with name '{name}', resolving conflicts")
                
                # Sort by priority (higher first), then by version (newer first)
                def sort_key(ext):
                    discovered = self._discovered_extensions.get(ext.name)
                    priority = discovered.priority if discovered else 0
                    # Simple version comparison (assumes semantic versioning)
                    version_parts = ext.version.split('.')
                    version_numeric = sum(int(part) * (1000 ** (2 - i)) 
                                        for i, part in enumerate(version_parts[:3]))
                    return (-priority, -version_numeric)
                    
                sorted_extensions = sorted(ext_list, key=sort_key)
                chosen_extension = sorted_extensions[0]
                resolved_extensions.append(chosen_extension)
                
                logger.info(f"Chose extension '{name}' v{chosen_extension.version} "
                          f"from {len(ext_list)} candidates")
                          
        return resolved_extensions
        
    def _update_config_stats(self, stats: DiscoveryStats, discovered: DiscoveredExtension):
        """Update statistics based on discovered extension type."""
        if discovered.config_type in ('yaml', 'json'):
            stats.config_driven_extensions += 1
        elif discovered.config_type == 'python':
            stats.python_extensions += 1
            
    def get_discovery_info(self) -> Dict[str, DiscoveredExtension]:
        """Get information about all discovered extensions."""
        return self._discovered_extensions.copy()
        
    def validate_extension_directory(self, extension_dir: Path) -> Tuple[bool, List[str]]:
        """
        Validate that a directory contains a valid extension.
        
        Returns:
            Tuple of (is_valid, list_of_issues)
        """
        issues = []
        
        if not extension_dir.exists():
            issues.append(f"Directory does not exist: {extension_dir}")
            return False, issues
            
        if not extension_dir.is_dir():
            issues.append(f"Path is not a directory: {extension_dir}")
            return False, issues
            
        # Check for any valid configuration files
        config_files = [
            extension_dir / 'extension.yaml',
            extension_dir / 'extension.yml', 
            extension_dir / 'extension.json',
            extension_dir / 'plugin.yaml',
            extension_dir / 'plugin.yml',
            extension_dir / 'plugin.json'
        ]
        
        python_files = [
            extension_dir / 'extension.py',
            extension_dir / 'plugin.py',
            extension_dir / '__init__.py'
        ]
        
        has_config = any(f.exists() for f in config_files)
        has_python = any(f.exists() for f in python_files)
        
        if not has_config and not has_python:
            issues.append("No valid extension configuration found "
                         "(expected extension.yaml/json or extension.py)")
                         
        # Try to load and validate
        discovered = self._discover_extension_in_path(extension_dir)
        if discovered and discovered.result != DiscoveryResult.SUCCESS:
            if discovered.error_message:
                issues.append(discovered.error_message)
                
        return len(issues) == 0, issues